import math
import urllib
import urllib2
import simplejson
import xml.sax
from PIL import Image, ImageFile


def request(page, params={}, progress=None, block=1024):
    """Makes a HTTP request to http://maps.google.com/[page]?[params].

    Returns:
    A tuple containing (data string, header dictionary).

    """
    # Open request URL.
    url = "http://maps.google.com/%s?%s" % (page, urllib.urlencode(params))
    resource = urllib2.urlopen(url)
    headers = resource.info()
    # Get request information.
    data = ""
    read = 0
    size = int(headers['content-length'])
     # Call 0 progress.
    if progress:
        progress(0, size)
    # Download block by block.
    while read < size:
        data += resource.read(block)
        read += block
        # Update progress.
        if progress:
            progress(min(read, size), size)
    return (data, headers)

def requestText(page, params={}, progress=None, block=1024):
    """Makes a HTTP request for raw data."""
    data, headers = request(page, params, progress, block)
    return data

def requestJpeg(page, params={}, progress=None, block=1024):
    """Makes a HTTP request for a JPEG image.

    Returns:
    The image as a PIL.Image.

    Throws:
    Exception if response content-type is not 'image/jpeg'.

    """
    data, headers = request(page, params, progress, block)
    # Ensure data is JPEG format.
    mime = headers['content-type'].lower()
    if mime != "image/jpeg":
        raise Exception(
            "Content type '%s' did not match 'image/jpeg'" % (mime))
    # Build PIL.Image object.
    parser = ImageFile.Parser()
    parser.feed(data)
    image = parser.close()
    return image

def getLocation(key, address, progress=None, block=1024):
    """Searches for a location.

    Parameters:
    key -- Google Maps API key to use for geocoder request.
    address -- Address string to search for.

    Returns:
    A Location object containing the information.

    Throws:
    GeocodeException if the search was not successful.

    """
    # Geocoding request.
    data = requestText(
        "maps/geo",
        {
            'output': "json",
            'oe': "utf-8",
            'q': address,
            'key': key,
            'callback': "cb"
        },
        progress,
        block)
    # Trim off Google's callback function wrappers.
    data = data[len("cb && cb("):-len(")")]
    json_loc = simplejson.loads(data)
    # Check result of request
    status = json_loc['Status']['code']
    if status not in GeoSuccess:
        raise GeocodeException(status)
    # Succeeded, extract data.
    placemark = json_loc['Placemark'][0]
    lng, lat = [float(x) for x in placemark['Point']['coordinates'][0:2]]
    return Location(lat, lng, placemark['address'])

GeoErrors = {
    500: "Server error",
    602: "Could not find geographic location",
    603: "Given address is unavailable",
    620: "Key has exceeded daily request limit"
}

GeoSuccess = [200]

class GeocodeException(Exception):
    
    """Describes an error that may occur in geocoding."""
    
    def __init__(self, code, msg=None):
        """Constructs a GeocodeException.

        Parameters:
        code -- The status returned in the HTTP request. eg 500, 602
        msg -- Optional text message. If not specified, this will be filled
          in for known error codes and set to "Unknown" otherwise.

        """
        self.code = code
        if not msg:
            try:
                self.msg = GeoErrors[code]
            except KeyError:
                self.msg = "Unknown"
        else:
            self.msg = msg

    def __str__(self):
        return "Geocode error (%i): %s" % (self.code, self.msg)

def getNearestPanoramaID(latlng, progress=None, block=1024):
    """Finds the nearest Street View panorama and returns its ID."""
    # Request nearest panoId.
    data = requestText(
        "cbk",
        {
            'output': "json",
            'oe': "utf-8",
            'cb_client': "api",
            'll': latlng.toUrlValue(),
            'callback': "cb"
        },
        progress,
        block)
    # Again with the trimming.
    data = data[len("cb && cb("):-len(")")]
    try:
        json_loc = simplejson.loads(data)
    except:
        raise NearestException(latlng)
    # Succeeded, return ID.
    return json_loc['Location']['panoId']

#def getNearestPanoramaDetails(latlng,
#        progress1=None, block1=1024,
#        progress2=None, block2=1024):
#    """Finds the nearest Street View panorama and gets its details."""
#    panoId = getNearestPanoramaID(latlng, progress1, block1)
#    return getPanoramaDetails(panoId, progress2, block2)

class NearestException(Exception):
    
    """Describes an error that may occur when searching for Street View."""
    
    def __init__(self, latlng):
        """Constructs a NearestException.

        Parameters:
        latlng -- Location information.

        """
        self.latlng = latlng

    def __str__(self):
        return ("Could not locate Street View near " +
            LatLng.__str__(self.latlng))

def getPanoramaDetails(panoId, progress=None, block=1024):
    """Retrieves the details of a Street View panorama.

    Parameters:
    panoId -- ID of the panorama.

    Returns:
    The details of the panorama.

    """
    # Request XML data.
    data = requestText(
        "cbk",
        {
            'output': "xml",
            'panoid': panoId
        },
        progress,
        block)
    # Parse XML data to PanoramaDetails.
    handler = PanoramaDetailsHandler()
    xml.sax.parseString(data, handler)
    details = handler.details
#    # Determine maximum zoom.
#    details.maxZoom = getPanoramaMaxZoom(details)
    return details

def getPanorama(panoId):
    """Downloads a panorama image at max zoom from ID.

    Parameters:
    panoId -- ID of the panorama.

    Returns:
    A PIL.Image of the panorama.

    """
    details = getPanoramaDetails(panoId)
    return getPanoramaImage(details)

def getPanoramaImage(details, zoom=None):
    """Downloads a panorama image from details, at specified zoom.

    Parameters:
    details -- Details of the panorama.
    zoom -- Level of zoom (resolution) to download.
      Must satisfy 0 <= zoom <= details.maxZoom.
      Defaults to details.maxZoom if None.

    Returns:
    A PIL.Image of the panorama.

    Throws:
    Exception if zoom is not within valid range specified by details.

    """
    # Check if zoom is valid.
    if zoom == None:
        zoom = details.maxZoom
    elif zoom < 0 or zoom > details.maxZoom:
        raise Exception(
            "Panorama zoom level not in valid range: %i" % (zoom))
    # Create an empty output image.
    w, h = details.sizeAtZoom(zoom)
    image = Image.new("RGB", (w, h), "black")
    # Loop over tiles.
    rows, cols = details.tilesAtZoom(zoom)
    for y in range(rows):
        for x in range(cols):
            tile = getPanoramaTile(details.id, zoom, x, y)
            image.paste(tile,
                (details.tileWidth * x, details.tileHeight * y))
    return image

# Code below no longer needed. Seems that zoom-0 height is always 208px.
#def getPanoramaMaxZoom(panoId):
#    """Works out the maximum zoom of a panorama."""
#    image = getPanoramaTile(panoId)
#    w, h = image.size
#    pixels = image.load()
#    for y in range(h-1, -1, -1):
#        for x in [0, w/2, w-1]:
#            if pixels[x, y] != (0, 0, 0):
#                break
#        else:
#            continue
#        break
#    else:
#        raise Exception("Edge of panorama could not be located")
#    height = y+1
#    return int(round(math.log(float(details.imageHeight) / height, 2)))

def getPanoramaTile(panoId, zoom=0, x=0, y=0, progress=None, block=1024):
    """Downloads a single tile in a panorama.

    Parameters:
    panoId -- Panorama ID.
    zoom -- Zoom level. Defaults to 0 (lowest resolution).

    Returns:
    A PIL.Image of the tile.

    """
    return requestJpeg(
        "cbk",
        {
            "output": "tile",
            "panoid": panoId,
            "zoom": zoom,
            "x": x,
            "y": y
        },
        progress,
        block)


class LatLng:
    
    """Contains latitude and longitude data.""" 

    def __init__(self, lat, lng):
        self.lat = lat
        self.lng = lng

    def __str__(self):
        return "(%s, %s)" % (self.latStr(), self.lngStr())

    def latStr(self, precision=6):
        return ("%." + str(precision) + "f") % self.lat

    def lngStr(self, precision=6):
        return ("%." + str(precision) + "f") % self.lng

    def toUrlValue(self, precision = 6):
        return self.latStr(precision) + "," + self.lngStr(precision)


class Location(LatLng):

    """Contains latitude and longitude data with a name string."""
    
    def __init__(self, lat, lng, address):
        LatLng.__init__(self, lat, lng)
        self.address = address

    def __str__(self):
        return "%s [\"%s\"]" % (LatLng.__str__(self), self.address)


class PanoramaDetails:
    
    """Contains all data about a Street View panorama."""

    def __init__(self, imageWidth=0, imageHeight=0, tileWidth=0, tileHeight=0,
        id="", lat=0, lng=0, title="", range="", projection="",
        panoYawDeg=0, tiltYawDeg=0, tiltPitchDeg=0, links=[], maxZoom=0):

        self.imageWidth = imageWidth
        self.imageHeight = imageHeight
        self.tileWidth = tileWidth
        self.tileHeight = tileHeight
        self.id = id
        self.lat = lat
        self.lng = lng
        self.title = title
        self.range = range
        self.projection = projection
        self.panoYawDeg = panoYawDeg
        self.tiltYawDeg = tiltYawDeg
        self.tiltPitchDeg = tiltPitchDeg
        self.links = links
        self.maxZoom = maxZoom

    def __str__(self):
        return "%s \"%s\" (%dx%d)" % (
            self.id,
            self.title,
            self.imageWidth,
            self.imageHeight)

    def size(self):
        return (self.imageWidth, self.imageHeight)

    def tileSize(self):
        return (self.tileWidth, self.tileHeight)

    def zoomRange(self):
        return range(self.maxZoom + 1)

    def sizeAtZoom(self, zoom):
        scale = 2 ** (self.maxZoom - zoom)
        return (self.imageWidth / scale, self.imageHeight / scale)

    def tilesForSize(self, size):
        imageWidth, imageHeight = size
        cols = int(math.ceil(float(imageWidth) / self.tileWidth))
        rows = int(math.ceil(float(imageHeight) / self.tileHeight))
        return (rows, cols)

    def tilesAtZoom(self, zoom):
        return self.tilesForSize(self.sizeAtZoom(zoom))

    def aspectRatio(self):
        return float(self.imageWidth) / self.imageHeight

    def zoomLevelNames(self):
        return ["%s x %s px" % self.sizeAtZoom(x) for x in self.zoomRange()]


class PanoramaDetailsHandler(xml.sax.handler.ContentHandler):
    
    """Used to dig PanoramaDetails out of XML response."""
    
    def __init__(self):

        self.details = PanoramaDetails()

        # State variables for XML parser
        self.isTextElement = False
        self.isStreetRangeElement = False
        self.isLinkTextElement = False
        self.nextLink = {}
        self.linkText = ""

    def startElement(self, name, attrs):
        if name == 'data_properties':
            self.details.imageWidth = int(attrs.get("image_width", ""))
            self.details.imageHeight = int(attrs.get("image_height", ""))
            self.details.maxZoom = int(round(math.log(
                float(self.details.imageHeight) / 208, 2)))
            self.details.tileWidth = int(attrs.get("tile_width", ""))
            self.details.tileHeight = int(attrs.get("tile_height", ""))
            self.details.id = attrs.get("pano_id")
            self.details.lat = float(attrs.get("lat"))
            self.details.lng = float(attrs.get("lng"))
        elif name == 'text':
            self.isTextElement = True
        elif name == 'street_range':
            self.isStreetRangeElement = True
        elif name == 'projection_properties':
            self.details.projection = attrs.get("projection_type", "")
            self.details.panoYawDeg = float(attrs.get("pano_yaw_deg", ""))
            self.details.tiltYawDeg = float(attrs.get("tilt_yaw_deg", ""))
            self.details.tiltPitchDeg = float(attrs.get("tilt_pitch_deg", ""))
        elif name == 'link':
            self.nextLink = {
                "yaw_deg": attrs.get("yaw_deg", ""),
                "pano_id": attrs.get("pano_id", ""),
                "road_argb": attrs.get("road_argb", ""),
                "text": ""}
        elif name == 'link_text':
            self.isLinkTextElement = True

    def endElement(self, name):
        if name == 'text':
            self.isTextElement = False
        elif name == 'street_range':
            self.isStreetRangeElement = False
        elif name == 'link_text':
            self.nextLink['text'] = self.linkText
            self.details.links.append(self.nextLink)
            self.linkText = ""
            self.isLinkNameElement = False

    def characters(self, ch):
        if self.isTextElement:
            self.details.title += ch
        elif self.isStreetRangeElement:
            self.details.range += ch
        elif self.isLinkTextElement:
            self.linkText += ch


